package it.antonio.ilpizze.aop;

import it.antonio.ilpizze.aop.util.AopUtil;
import it.antonio.ilpizze.reflection.Clazz;
import it.antonio.ilpizze.reflection.ConstructorAccessor;
import it.antonio.ilpizze.reflection.MethodAccessor;

import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import net.sf.cglib.core.CodeGenerationException;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class CGLibProxyFactory implements ProxyFactory {

	@Override
	@SuppressWarnings("unchecked")
	public <T> T proxifyInstance(Class<T> clazz, Object[] params, List<Aspect> aspects) {
		try {
			

			// Configure CGLIB Enhancer...
			Enhancer enhancer = new Enhancer();
			
			enhancer.setSuperclass(clazz);
			enhancer.setInterceptDuringConstruction(false);

			enhancer.setCallback(new CGLibMethodInterceptor(clazz, aspects));

			// Generate the proxy class and create a proxy instance.
			return (T) createInstance(enhancer, clazz, params);
		} catch (CodeGenerationException ex) {
			throw new AopException("Could not generate CGLIB subclass of class [" +	clazz + "]: " +	"Common causes of this problem include using a final class or a non-visible class",	ex);
		} catch (IllegalArgumentException ex) {
			throw new AopException("Could not generate CGLIB subclass of class [" + clazz + "]: " + "Common causes of this problem include using a final class or a non-visible class",	ex);
		} catch (Exception ex) {
			throw new AopException("Unexpected AOP exception: " + ex.getMessage(), ex);
		}
	}

	@Override
	@SuppressWarnings("unchecked")
	public <T> Class<T> proxifyClass(Class<T> clazz, List<Aspect> aspects) {
		Enhancer enhancer = new Enhancer();
		
		enhancer.setSuperclass(clazz);
		enhancer.setInterceptDuringConstruction(false);
		enhancer.setCallbackType(MethodInterceptor.class);
		   
		Class<T> generated = enhancer.createClass();
		Enhancer.registerCallbacks(generated, new Callback[]{new CGLibMethodInterceptor(clazz, aspects)});
		
		return generated;

	}
	
	private Object createInstance(Enhancer enhancer, Class<?> clazz, Object[] params){
		Clazz<?> descr = Clazz.get(clazz);
		
		if(params.length == 0){
			// no args
			List<ConstructorAccessor> constructors = descr.findConstructors();
			if(constructors.size() > 0){
				// default constructor
				ConstructorAccessor c = constructors.get(0);
				Class<?>[] constrParams = c.getParameterTypes();
				return enhancer.create(constrParams, new Object[constrParams.length]);
			} else {
				// interface: no constructors
				return enhancer.create();	
			}
		} else {
			// args
			List<ConstructorAccessor> constructors = descr.findConstructors();
			for (ConstructorAccessor c : constructors) {
				Class<?>[] constrParams = c.getParameterTypes();
				if (params.length == constrParams.length) {
					boolean check = true;
					for (int i = 0; i < params.length; i++) {
						check = check && constrParams[i].isAssignableFrom(params[i].getClass());
					}
					if (check) {
						return enhancer.create(constrParams, params);
					}
				}
			}
		}
		throw new AopException("Cannot create instance of " + clazz.getSimpleName());
	}
	
	
	
	private class CGLibMethodInterceptor implements MethodInterceptor{
		private Class<?> clazz;
		private List<Aspect> aspects;
		
		public CGLibMethodInterceptor(Class<?> clazz, List<Aspect> aspects) {
			this.clazz = clazz;
			this.aspects = aspects;
		}


		@Override
		public Object intercept(Object instance, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
			
			MethodAccessor methodAccessor = new MethodAccessor(method);
			
			if(AopUtil.isObjectMethod(methodAccessor)){
				if(clazz.isInterface()){
					return null;    
				} else {
					return methodProxy.invokeSuper(instance, args);
            	 
				}
			}
			
			List<Advice> advices = new LinkedList<Advice>();
			for(Aspect aspect: aspects){
				if(aspect.pointcut().apply(methodAccessor)) {
					advices.add(aspect.advice());
				}
			}
			Invocation inv = new CGLibInvocation(clazz.isInterface(), advices, instance, methodAccessor, methodProxy, args);
			return inv.proceed();
		}
		
	}
	
	
	
	private class CGLibInvocation implements Invocation{
		
        private Object instance;
        private MethodAccessor method;
        private Object[] arguments;
        private Iterator<Advice> advices;
        private MethodProxy proxy;
        private boolean isInterface;
        
        public CGLibInvocation(boolean isInterface, List<Advice> advices, Object instance, MethodAccessor method, MethodProxy proxy, Object[] arguments) {
                super();
                this.instance = instance;
                this.method = method;
                this.arguments = arguments;
                this.advices = advices.iterator();
                this.proxy = proxy;
                this.isInterface = isInterface;
        }

        public MethodAccessor getMethod() {
                return method;
        }

        public Object[] getArguments() {
                return arguments;
        }
        
        public Object getInstance() {
                return instance;
        }

        
		public Object proceed() throws Throwable{
				 while(advices.hasNext()){
                        Advice advice = advices.next();
                        return advice.onMethod(this);
                }
                
                // if it has an implementation try to call the original method
                if(!isInterface){
                	     return proxy.invokeSuper(instance, arguments);
                } else {
                	 return null;
                }
                
        }
	}



	
	
	
	
}
